// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/upbit/api_order/native_private_client.h"

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/market/enums.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/order/upbit.pb.h"
#include "coin2/exchange/upbit/api_util/auth_key.h"
#include "coin2/exchange/upbit/api_util/rate_tracker.h"
#include "coin2/exchange/upbit/symbology/product.h"
#include "coin2/exchange/upbit_id/symbology/product.h"
#include "coin2/exchange/upbit_sg/symbology/product.h"
#include "coin2/exchange/upbit_th/symbology/product.h"

using namespace coin::proto;

const MarketExchangeApi UpbitNativePrivateClient::UPBIT_KR = MarketExchangeApi{Spot, Upbit, "v1"};
const MarketExchangeApi UpbitNativePrivateClient::UPBIT_ID = MarketExchangeApi{Spot, Upbitid, "v1"};
const MarketExchangeApi UpbitNativePrivateClient::UPBIT_SG = MarketExchangeApi{Spot, Upbitsg, "v1"};
const MarketExchangeApi UpbitNativePrivateClient::UPBIT_TH = MarketExchangeApi{Spot, Upbitth, "v1"};

UpbitNativePrivateClient::UpbitNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config) {
  mea_ = mea;
  NativeClientConfig cfg;
  if (mea_ == UPBIT_KR) {
    cfg.set_rest_host("api.upbit.com");
  } else if (mea_ == UPBIT_ID) {
    cfg.set_rest_host("id-api.upbit.com");
  } else if (mea_ == UPBIT_TH) {
    cfg.set_rest_host("th-api.upbit.com");
  } else if (mea_ == UPBIT_SG) {
    cfg.set_rest_host("sg-api.upbit.com");
  } else {
    NOTREACHED() << "unsupported mea. " << mea_.String();
  }
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[UpbitNativePrivateClient] config: " << cfg.DebugString();
  auths_.Construct<UpbitRestAuth, NativeClientConfig>(cfg);
  using RateTracker = coin2::exchange::upbit::api_util::RateTracker;
  rate_tracker_ = std::make_unique<RateTracker>(mea_, cfg);
}

auto UpbitNativePrivateClient::QueryAccountBalance() -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryBalance failed";
    return {};
  }
  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_account_balance_, &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return HttpContextPtr(context);
}

auto UpbitNativePrivateClient::QueryOrders(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryOrders failed";
    return {};
  }
  UrlParamsBuilder builder;

  std::string state;
  if (params.state() == "open") {
    state = "wait";
  } else {
    state = params.state();
  }

  if (params.has_market()) {
    builder.PutGet("market", params.market());
  }
  if (params.has_state()) {
    builder.PutGet("state", state);
  }
  if (params.has_page()) {
    builder.PutGet("page", params.page());
  }
  if (params.has_order_by()) {
    builder.PutGet("order_by", params.order_by());
  }
  if (params.uuids_size() > 0 || params.identifiers_size() > 0) {
    std::string external_query_string;
    if (params.uuids_size() > 0) {
      external_query_string.append(ToList(params.uuids(), "uuids[]", "&"));
    }
    if (params.identifiers_size() > 0) {
      if (!external_query_string.empty()) {
        external_query_string.append("&");
      }
      external_query_string.append(ToList(params.identifiers(), "identifiers[]", "&"));
    }
    GetAuth()->SetExternalQueryString(external_query_string);
  }
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_orders_, &builder);
  GetAuth()->SetExternalQueryString("");
  Push(1, ts, ActionType::QUERY);

  if (params.state() == "open") {
    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  } else {
    context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  }
  return HttpContextPtr(context);
}

auto UpbitNativePrivateClient::QueryTrades(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit. QueryTrades failed";
    return {};
  }
  UrlParamsBuilder builder;
  if (params.has_uuid()) {
    builder.PutGet("uuid", params.uuid());
  }
  if (params.has_identifier()) {
    builder.PutGet("identifier", params.identifier());
  }
  auto context = GetAuth()->CreateRequestByGetWithSignature(path_query_trade_, &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return HttpContextPtr(context);
}

auto UpbitNativePrivateClient::PlaceOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    LOG(ERROR) << "Hit rate limit. PlaceOrder failed";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("market", params.market());
  builder.PutPost("side", params.side());
  builder.PutPost("volume", params.volume());
  builder.PutPost("price", params.price());
  builder.PutPost("ord_type", params.ord_type());
  if (params.has_identifier()) {
    builder.PutPost("identifier", params.identifier());
  }
  auto context = GetAuth()->CreateRequestByPostWithSignature(path_place_order_, &builder);
  Push(1, ts, ActionType::PLACE);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return HttpContextPtr(context);
}

auto UpbitNativePrivateClient::CancelOrder(const RestParams& params) -> HttpContextPtr {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    LOG(ERROR) << "Hit rate limit. CancelOrder failed";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("uuid", params.uuid());
  auto context = GetAuth()->CreateRequestByDeleteWithSignature(path_cancel_order_, &builder);
  Push(1, ts, ActionType::CANCEL);

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return HttpContextPtr(context);
}

auto UpbitNativePrivateClient::CancelAllOrder(const RestParams& params) -> HttpContextPtr {
  LOG(ERROR) << "Hit rate limit. CancelAllOrder failed";
  return {};
}

auto UpbitNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

auto UpbitNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

auto UpbitNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  using namespace coin::proto;

  std::string volume;
  std::string price;
  if (mea_ == UPBIT_KR) {
    using UpbitProduct = coin2::exchange::upbit::symbology::UpbitProduct;
    auto product = UpbitProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    volume = formatter.FormatQty(order_spec.qty);
    price = formatter.FormatPrice(order_spec.price);
  } else if (mea_ == UPBIT_ID) {
    using UpbitProduct = coin2::exchange::upbit_id::symbology::UpbitIdProduct;
    auto product = UpbitProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    volume = formatter.FormatQty(order_spec.qty);
    price = formatter.FormatPrice(order_spec.price);
  } else if (mea_ == UPBIT_SG) {
    using UpbitProduct = coin2::exchange::upbit_sg::symbology::UpbitSgProduct;
    auto product = UpbitProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    volume = formatter.FormatQty(order_spec.qty);
    price = formatter.FormatPrice(order_spec.price);
  } else if (mea_ == UPBIT_TH) {
    using UpbitProduct = coin2::exchange::upbit_th::symbology::UpbitThProduct;
    auto product = UpbitProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    volume = formatter.FormatQty(order_spec.qty);
    price = formatter.FormatPrice(order_spec.price);
  } else {
    NOTREACHED() << "unsupported mea. " << mea_.String();
  }

  RestParams params;
  params.set_market(order_spec.product);
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("bid");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("ask");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    params.set_ord_type("limit");
    params.set_volume(volume);
    params.set_price(price);
  } else {
    NOTREACHED() << "unsupported order type: " << static_cast<int>(order_spec.order_type);
  }

  return params;
}

auto UpbitNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  RestParams params;
  params.set_uuid(order_context.exchange_order_id());

  return params;
}

std::shared_ptr<AsioHttpContext> UpbitNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> UpbitNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_state("open");
      return QueryOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}
